Skip to content

Blocks 中的状态 (State in Blocks)

在使用 gr.Blocks() 构建 Gradio 应用程序时,您可能需要在用户之间共享某些值(例如,网页访问计数器),或者为单个用户在不同交互之间保持某些值(例如,聊天历史记录)。这被称为状态管理,在 Gradio 应用程序中有三种管理状态的方式:

  • 全局状态(Global State):在 Gradio 应用程序运行期间,在所有用户之间保持并共享值。
  • 会话状态(Session State):在单个用户使用 Gradio 应用程序的单次会话期间保持值。如果用户刷新页面,会话状态将被重置。
  • 浏览器状态(Browser State):将值保存在浏览器的本地存储中,即使页面刷新或关闭后,数据仍然可以保持。

全局状态 (Global State)

Gradio 应用中的全局状态非常简单:在函数外部创建的任何变量都会在所有用户之间全局共享。

这使得管理全局状态变得非常简单,且无需外部服务。例如,在以下应用程序中,visitor_count 变量在所有用户之间共享:

python
import gradio as gr

# 在所有用户之间共享
visitor_count = 0

def increment_counter():
    global visitor_count
    visitor_count += 1
    return visitor_count

with gr.Blocks() as demo:    
    number = gr.Textbox(label="访问者总数", value="计数中...")
    demo.load(increment_counter, inputs=None, outputs=number)

demo.launch()

这意味着,如果您不希望在用户之间共享某个值,则应该在函数内部声明它。但是,如果您需要在函数调用之间共享值(例如聊天历史记录),那么应该使用以下状态管理方法之一。

会话状态 (Session State)

Gradio 支持会话状态,数据可以在页面会话中的多次提交之间保持。需要再次强调,会话数据不会在应用程序的不同用户之间共享,如果用户刷新页面重新加载 Gradio 应用程序,数据也不会保持。要在会话状态中存储数据,您需要做三件事:

  1. 创建一个 gr.State() 对象。如果这个状态对象有默认值,请将其传递给构造函数。注意,gr.State 对象必须是可深度复制的,否则您需要使用不同的方法(如下所述)。
  2. 在事件监听器中,根据需要将 State 对象作为输入和输出。
  3. 在事件监听器函数中,添加变量到输入参数和返回值中。

让我们看一个简单的例子。下面是一个简单的购物车应用,您可以向购物车中添加商品,并查看购物车的大小:

python
import gradio as gr

with gr.Blocks() as demo:
    cart = gr.State([])  # 初始化为空列表
    items_to_add = gr.CheckboxGroup(["谷物", "牛奶", "橙汁", "水"])

    def add_items(new_items, previous_cart):
        cart = previous_cart + new_items
        return cart

    gr.Button("添加商品").click(add_items, [items_to_add, cart], cart)

    cart_size = gr.Number(label="购物车大小")
    cart.change(lambda cart: len(cart), cart, cart_size)

demo.launch()

请注意我们如何使用状态:

  1. 我们在 gr.State() 对象中存储购物车商品,初始化为空列表。
  2. 向购物车添加商品时,事件监听器同时使用购物车作为输入和输出 - 它返回更新后的包含所有商品的购物车。
  3. 我们可以为购物车添加 .change 监听器,同样将状态变量作为输入。

您可以将 gr.State 视为一个不可见的 Gradio 组件,可以存储任何类型的值。在这里,cart 在前端不可见,但用于计算。

对于状态变量,当任何事件监听器更改状态变量的值时,.change 监听器将被触发。如果状态变量包含序列(如 listsetdict),当其中的任何元素发生变化时,都会触发更改。如果它包含对象或基本类型,当值的哈希发生变化时,会触发更改。因此,如果您定义了一个自定义类并创建了该类实例的 gr.State 变量,请确保该类包含合理的 __hash__ 实现。

当用户刷新页面时,会话状态变量的值会被清除。在用户关闭标签页后,该值会在应用后端存储 60 分钟(这可以通过 gr.Blocksdelete_cache 参数配置)。

处理不可深度复制的对象

如前所述,存储在 gr.State 中的值必须是可深度复制的。如果您使用的是不能深度复制的复杂对象,可以采用不同的方法,手动读取用户的 session_hash 并为每个用户存储一个全局 dictionary,其中包含您对象的实例。以下是操作方法:

python
import gradio as gr

class NonDeepCopyable:
    def __init__(self):
        from threading import Lock
        self.counter = 0
        self.lock = Lock()  # Lock 对象不能被深度复制
    
    def increment(self):
        with self.lock:
            self.counter += 1
            return self.counter

# 存储用户特定实例的全局字典
instances = {}

def initialize_instance(request: gr.Request):
    instances[request.session_hash] = NonDeepCopyable()
    return "会话已初始化!"

def cleanup_instance(request: gr.Request):
    if request.session_hash in instances:
        del instances[request.session_hash]

def increment_counter(request: gr.Request):
    if request.session_hash in instances:
        instance = instances[request.session_hash]
        return instance.increment()
    return "错误:会话未初始化"

with gr.Blocks() as demo:
    output = gr.Textbox(label="状态")
    counter = gr.Number(label="计数器值")
    increment_btn = gr.Button("增加计数器")
    increment_btn.click(increment_counter, inputs=None, outputs=counter)
    
    # 页面加载时初始化实例
    demo.load(initialize_instance, inputs=None, outputs=output)    
    # 页面关闭/刷新时清理实例
    demo.close(cleanup_instance)    

demo.launch()

浏览器状态 (Browser State)

Gradio 还支持浏览器状态,即使在页面刷新或关闭后,数据也会保存在浏览器的 localStorage 中。这对于存储用户偏好、设置、API 密钥或其他应该在会话之间保持的数据非常有用。要使用浏览器状态:

  1. 创建一个 gr.BrowserState 对象。您可以选择提供初始默认值和标识浏览器 localStorage 中数据的键。
  2. 在事件监听器中,将其作为常规 gr.State 组件用作输入和输出。

以下是一个在会话之间保存用户名和密码的简单示例:

python
import random
import string
import gradio as gr
import time

with gr.Blocks() as demo:
    gr.Markdown("您的用户名和密码将保存在浏览器的本地存储中。"
                "如果您刷新页面,这些值将被保留。")
    username = gr.Textbox(label="用户名")
    password = gr.Textbox(label="密码", type="password")
    btn = gr.Button("随机生成")
    local_storage = gr.BrowserState(["", ""])
    saved_message = gr.Markdown("✅ 已保存到本地存储", visible=False)

    @btn.click(outputs=[username, password])
    def generate_randomly():
        u = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        p = "".join(random.choices(string.ascii_letters + string.digits, k=10))
        return u, p

    @demo.load(inputs=[local_storage], outputs=[username, password])
    def load_from_local_storage(saved_values):
        print("从本地存储加载", saved_values)
        return saved_values[0], saved_values[1]

    @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])
    def save_to_local_storage(username, password):
        return [username, password]

    @gr.on(local_storage.change, outputs=[saved_message])
    def show_saved_message():
        timestamp = time.strftime("%I:%M:%S %p")
        return gr.Markdown(
            f"✅ 已在 {timestamp} 保存到本地存储",
            visible=True
        )

demo.launch()

注意:存储在 gr.BrowserState 中的值在 Gradio 应用重启时不会保持。要保持它,可以在 gr.BrowserState 组件中硬编码特定的 storage_keysecret 值,并在相同的服务器名称和服务器端口上重新启动 Gradio 应用。但是,只有在运行受信任的 Gradio 应用时才应该这样做,因为原则上,这可以允许一个 Gradio 应用访问由另一个 Gradio 应用创建的 localStorage 数据。

状态管理的最佳实践

在选择状态管理方法时,请考虑以下最佳实践:

  1. 全局状态:适用于需要在所有用户之间共享的数据,如访问计数器或全局设置。
  2. 会话状态:对于特定于用户的数据,如当前会话的交互历史,但不需要长期保持。
  3. 浏览器状态:对于应该在浏览器会话之间保持的用户特定数据,如个人偏好或认证令牌。

根据您的应用程序需求选择合适的状态管理方法,可以提高用户体验并简化代码。

资源清理

长时间运行的应用程序可能会积累大量状态数据。Gradio 提供了一些工具来帮助清理资源:

  1. 自动删除 gr.State:当用户关闭浏览器标签页时,Gradio 会在 60 分钟后自动删除与该用户会话关联的任何 gr.State 变量。
  2. 自动缓存清理:使用 delete_cache 参数可以定期清理临时文件。
  3. Blocks.unload 事件:可以在用户断开连接时运行任意清理函数。
python
import gradio as gr

with gr.Blocks(delete_cache=(86400, 86400)) as demo:  # 每天删除超过一天的临时文件
    # 应用程序代码...
    
    # 用户关闭标签页时清理资源
    demo.unload(lambda: print("用户已离开,清理资源"))

demo.launch()

结论

状态管理是构建复杂 Gradio 应用程序的关键方面。通过了解全局状态、会话状态和浏览器状态之间的差异,您可以为您的应用程序选择最合适的方法,从而创建更加动态和响应式的用户体验。

在下一章中,我们将探讨如何像函数一样使用 Blocks,这使您可以轻松组合和重用功能组件。